TIKA-4766: typed Document parse contract for tika-grpc#2921
Conversation
c1de042 to
1d2d5bd
Compare
…t contract One small, stable proto instead of a message per source format. Contract (tika-grpc-api, ~200 lines of proto): - Document: content as a structured markdown block tree (headings, paragraphs, lists, tables, code blocks, inline runs -- CommonMark + GFM, the same markdown ToMarkdownContentHandler already renders), plus `markdown` as the authoritative rendered form. - DocumentMetadata: a small bounded set of typed common fields grouped by concern, not by source format (title/authors/keywords/languages, created and modified as Timestamps, page/word/character counts, dimensions, rights). - Tagged tail: `extra` carries every remaining Tika key losslessly and multivalue-preserving, typed only where Tika's own Property declares a type (integer/real/boolean/date) and string otherwise -- never guessed. - `embedded` recurses: a parent PDF and an embedded image are each a fully typed Document. - `format_category` is a cheap routing hint; cross-cutting concerns such as Creative Commons rights coexist with it rather than fighting a oneof. Format specifics live in per-parser DocumentTransformer code (tika-grpc-mapper), not in the wire contract: adding or changing a parser never touches the proto, and metadata churn lands in the mapper and the tagged tail, so a new or renamed metadata key never forces a client rebuild. Server (tika-grpc): - FetchAndParseReply.document (field 5) replaces the fields map (field 2, now reserved). TikaGrpcServerImpl maps parse output via DocumentBuilder. Modules: tika-grpc-api (proto + generated messages + bundled FileDescriptorSet), tika-grpc-mapper (DocumentBuilder, per-format transformers, markdown block-tree builder), both listed in tika-bom. Tests: transformer tests against real parse fixtures per format, block tree round-trip tests, DocumentBuilder envelope/status/embedded tests, server tests updated to read FetchAndParseReply.document.
1d2d5bd to
da51be9
Compare
|
@nddipiazza @tballison what do you think of this design instead? far less fields - ability to roll your own protobuf model in the future. Best of both worlds. Document structure is very markdown-friendly. I'll make the output of this be able to be the input of the grpc OpenNLP grpc server. |
nddipiazza
left a comment
There was a problem hiding this comment.
Review: e2e test coverage for the new typed Document contract
Reviewed the design overall — the shift from mirroring Tika's metadata taxonomy in protobuf to a small, stable document.proto (208 lines) plus per-parser mapper code is a solid answer to the #2916 feedback. A few things worth addressing before merge:
Testing gap: e2e coverage does not exercise the new contract
The only change in tika-e2e-tests/tika-grpc is a 2-line fix in HandlerTypeTest.java:
- String htmlContent = htmlReply.getFieldsMap().get("X-TIKA:content");
+ String htmlContent = htmlReply.getDocument().getMarkdown();(same for the TEXT handler case). This is a compile-fix to keep the pre-existing assertion working against the new API — it only proves document.markdown is non-empty over a real gRPC round-trip for two handler types.
It does not exercise, end-to-end, through the live server:
DocumentMetadatatyped fields (title/authors/dates/counts) — no e2e assertion anywhereextratagged-tail typing (int/bool/date/string) — no e2e assertionblocksstructured content tree (headings/tables/lists/code) — untouched by any e2e testembeddedrecursion for container formats (e.g. an Office doc with an embedded image) — no e2e coverage at allformat_categoryrouting hint — untouched
The other e2e tests (FileSystemFetcherTest, IgniteConfigStoreTest, ExternalTestBase) only ever inspect getFetchKey()/getStatus() on FetchAndParseReply — none of them look at getDocument(), so the bulk-corpus/streaming/ignite e2e paths give zero signal on the new typed contract.
The real proof of correctness for the mapping logic lives entirely in tika-grpc-mapper's unit tests (DocumentBuilderTest, MarkdownBlockTreeBuilderTest, one test class per transformer), which feed fixture-derived Metadata/markdown into DocumentBuilder in-process. That's good for the mapping logic itself, but it bypasses the actual gRPC wire serialization, the live server (TikaGrpcServerImpl), the pipes client, and fetcher plumbing entirely.
Ask: add (or extend HandlerTypeTest) at least one e2e case per format that fetches a real file through the live gRPC server and asserts on document.metadata (a couple of typed fields), document.extra (at least one tagged key), and one document.embedded case (e.g. an Office/PDF file with an embedded image) — not just that markdown is non-empty. Right now this new, larger surface area has no live-server coverage at all.
Other findings from code inspection
- Likely-dead error path:
DocumentBuilder.build()'sprimary == nullbranch (returnsFAILEDwith "No metadata returned from parse") looks unreachable in production — the only caller,TikaGrpcServerImpl, always passes a non-nullMetadata(tikaMetadata = new Metadata()as fallback whenemitData()/metadata list is empty). Worth double-checking this is intentional, since a real fetch failure with no emit data currently produces an "empty-but-success-shaped"Documentrather than hitting this explicit error branch. - Duplication: 7 of 8 format transformers (
Generic,Pdf,Html,Image,Office,Rtf,Epub) repeat an identical block mapping TITLE/DESCRIPTION/CREATOR/SUBJECT/LANGUAGE/CREATED/MODIFIED. Consider factoring this into a shared helper inTransformSupportcalled by each transformer, keeping only the format-specific additions per-transformer. - Minor:
parseTimeMsinTikaGrpcServerImplmeasures fetch+parse combined (timer starts beforepipesClient.process()), though the field's doc/intent reads as parse-only time.
Nice work on the overall shape of the contract — the main blocker from my read is the e2e coverage gap above.
nddipiazza
left a comment
There was a problem hiding this comment.
Overall this is a solid design and a much better-scoped answer to the #2916 feedback: a small stable 208-line proto, format logic pushed into per-parser Java transformers, and a lossless typed+tagged tail. Requesting changes mainly on test coverage for the new contract, plus a couple of correctness/maintainability issues found while reading the mapper code (see inline comments).
E2E coverage (the review's main ask): HandlerTypeTest now exercises the typed contract through the live gRPC server -- typed DocumentMetadata fields, tagged-tail typing (pdf:encrypted arrives as a boolean, not a string), the block tree, format_category, and embedded recursion for a container docx. New fixtures: testPDF.pdf, test_recursive_embedded.docx. Bug found by the new e2e tests: Property.get() only sees property constants whose declaring class is initialized, and the parse runs in a forked JVM -- so tagged-tail typing silently degraded to strings in the server. MetadataTagger now force-initializes the tika-core property vocabularies so tagging is deterministic. Also fixed while here: the e2e configs now opt in to allowComponentManagement/allowPerRequestConfig, without which every e2e test dies with PERMISSION_DENIED since the TIKA-4764 security gating. DocumentBuilder: the primary==null branch is now reachable -- the server passes null when the pipes result carried no metadata instead of an empty Metadata that produced a success-shaped Document on fetch failures. Status still derives from the pipes status (EMPTY_OUTPUT with no metadata is a success, not an error). Dedup: the common field block (title/description/authors/keywords/ languages/created/modified) now lives in TransformSupport.mapCommonFields, used by six transformers; Office/RTF keep meta:keyword as their keyword source via an overload. RTF gains the dc:language mapping it had drifted away from. Image stays bespoke on purpose (IPTC/EXIF sources). Renamed ParseStatus.parse_time_ms to fetch_parse_time_ms: the timer wraps the whole pipesClient.process() round trip, and fetch+parse are not separable server-side.
|
@nddipiazza great feedback..
My bad; the e2e suite really was only asserting "markdown is non-empty". Added three live-server cases to
Two things fell out of writing these:
Note the CI e2e job doesn't currently execute these (the |
|
Thanks for the review @nddipiazza, all four points addressed:
Also merged latest main (the lombok removal and split-packages changes). Full build plus mapper, grpc, and e2e suites pass locally. |
Summary
Follow-up to #2916, reshaped per the review there. Instead of mirroring Tika's open metadata taxonomy in protobuf (~5k lines of proto, per-format messages), this PR types the thing that is actually stable: the parsed document. One small contract —
document.protois 208 lines — and format specifics live in per-parser mapping code, never in the wire.FetchAndParseReply.fields(map<string,string>, field 2, now reserved) is replaced byFetchAndParseReply.document.How this answers the #2916 review
extratail), not schema — add/rename/retype a TikaPropertyand no client regenerates anythingDocument.extracarries every Tika key, multivalue-preserving; a test asserts nothing is droppedDocumentMetadatatypes only the bounded cross-format fields (title, authors, dates asTimestamp, counts, dimensions, rights)The shape
markdown(the same renderToMarkdownContentHandleralready produces since TIKA-4730) plusblocks— that markdown parsed once, format-agnostically, into a structured tree of headings/paragraphs/lists/tables/code blocks/inline runs (CommonMark + GFM, a spec that does not churn). This is what a downstream NLP/RAG/embeddings consumer actually wants: typed tables and sections, not a string to re-parse.DocumentMetadata, grouped by concern, not by source format. Dates areTimestamps, counts are ints — not strings that 12 language clients each re-parse.extra— every remaining key, typed only where Tika's ownPropertydeclares a type (integer/real/boolean/date), string otherwise, never guessed.embeddedrecurses: a PDF with an embedded image is a parentDocumentwith a fully typed child — no forcing two formats into one bucket (this was the oneof problem from TIKA-4766: Typed parse response grpc #2916).DocumentTransformer(seetika-grpc-mapper/docs/EXTENSIONS.md);PdfDocumentTransformeris 65 lines and the wire contract does not move.Deliberately not in this PR (follow-ups, each its own PR)
Documentas agoogle.protobuf.Any— so wildly different result shapes (e.g. a document-layout model's tree) never require Tika to model them. Built and tested on a branch; kept out to keep this reviewable..mdinput files (separate JIRA).optionalfields, compatible both directions.Open decisions where reviewer preference wins
repeated MetadataFieldwith a typed value oneof (as implemented) vs themap<string, StringList>suggested in TIKA-4766: Typed parse response grpc #2916. The typed-where-declared tail preserves types without guessing; the map is maximally churn-proof. Swapping is a one-message change — happy to go either way.markdown+blocksboth: today both ship (string render + structured tree). If payload size matters, a per-request flag choosing one is easy.fieldsis hard-removed (4.0, nothing consumes it yet); can switch to deprecate-then-remove if preferred.Client migration
fields["X-TIKA:content"]document.markdown(or walkdocument.blocks)fields["Content-Type"]document.content_typedocument.metadata.title/.authors/.created(Timestamp)document.extra(typed by declaredPropertytype, string otherwise)Test plan
./mvnw -pl tika-grpc-api,tika-grpc-mapper,tika-grpc test— green (transformer tests against real parse fixtures per format, block-tree tests,DocumentBuilderenvelope/status/embedded tests, server tests readingFetchAndParseReply.document)tika-grpc-apijar bundlesMETA-INF/org.apache.tika.grpc.v1.descriptors(verified: containsdocument.proto)tika-grpc-e2e-testcompiles against the new APIDownstream context: this contract is what the OpenNLP gRPC work (OPENNLP-1833) will consume as input — Tika parse → typed document → NLP/embeddings without re-parsing strings.